Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/cloudflare/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,15 +65,15 @@
"@sentry/server-utils": "10.65.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
"@cloudflare/workers-types": "^4.x || ^5.x"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
"optional": true
}
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@cloudflare/workers-types": "5.20260710.1",
"@types/node": "^18.19.1",
"wrangler": "4.61.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, debug, ServerRuntimeClient, spanIsSampled } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import type { ExecutionContextCompat } from './executionContext';
import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';

Expand DownExpand Up@@ -230,7 +231,7 @@ interface BaseCloudflareOptions {
* @see @sentry/core Options for more information.
*/
export interface CloudflareOptions extends Options<CloudflareTransportOptions>, BaseCloudflareOptions {
ctx?: ExecutionContext;
ctx?: ExecutionContextCompat;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare/src/executionContext.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import type { ExecutionContext } from '@cloudflare/workers-types';

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

type FlushLock = {
readonly ready: Promise<void>;
Expand DownExpand Up@@ -34,7 +35,7 @@ const flushLockRegistries = new WeakMap<ExecutionContext['waitUntil'], FlushLock
*
* By using the original waitUntil for flush operations, we bypass this issue.
*/
export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContext['waitUntil'] | undefined {
export function getOriginalWaitUntil(context: ExecutionContextCompat): ExecutionContext['waitUntil'] | undefined {
// eslint-disable-next-line @typescript-eslint/unbound-method
const currentWaitUntil = context.waitUntil;
const original = flushLockRegistries.get(currentWaitUntil)?.originalWaitUntil;
Expand All@@ -49,7 +50,7 @@ export function getOriginalWaitUntil(context: ExecutionContext): ExecutionContex
* @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
* @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
*/
export function makeFlushLock(context: ExecutionContext): FlushLock {
export function makeFlushLock(context: ExecutionContextCompat): FlushLock {
const registry = getOrCreateFlushLockRegistry(context);
let resolveAllDone: () => void = () => undefined;
const allDone = new Promise<void>(res => {
Expand DownExpand Up@@ -81,7 +82,7 @@ export function makeFlushLock(context: ExecutionContext): FlushLock {
return Object.freeze(lock);
}

function getOrCreateFlushLockRegistry(context: ExecutionContext): FlushLockRegistry {
function getOrCreateFlushLockRegistry(context: ExecutionContextCompat): FlushLockRegistry {
// eslint-disable-next-line @typescript-eslint/unbound-method
const waitUntil = context.waitUntil;
const existingRegistry = flushLockRegistries.get(waitUntil);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,7 +69,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
if (prop === 'send') {
const original = Reflect.get(target, prop, receiver) as Queue['send'];

return function (this: unknown, message: unknown, options?: QueueSendOptions): Promise<void> {
return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType<Queue['send']> {
return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () =>
Reflect.apply(original, target, [message, options]),
);
Expand All@@ -82,7 +82,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, bindingName:
this: unknown,
messages: Iterable<MessageSendRequest>,
options?: QueueSendBatchOptions,
): Promise<void> {
): ReturnType<Queue['sendBatch']> {
const messageArray = Array.from(messages);
const totalBodySize = messageArray.reduce<number | undefined>((acc, m) => {
const size = getBodySize(m.body);
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare/src/pages-plugin.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { wrapRequestHandler } from './request';

/**
Expand DownExpand Up@@ -53,8 +54,9 @@ export function sentryPagesPlugin<
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
);
// A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only
// uses `waitUntil` and a `'storage' in context` check, both of which this satisfies.
const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat;
return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next());
};
}
7 changes: 4 additions & 3 deletions packages/cloudflare/src/request.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import type { CfProperties, ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/workers-types';
import {
captureException,
continueTrace,
Expand All@@ -14,20 +14,21 @@ import {
} from '@sentry/core';
import { captureIncomingRequestBody } from './integrations/httpServer';
import type { CloudflareOptions } from './client';
import type { ExecutionContextCompat } from './executionContext';
import { flushAndDispose, getOriginalWaitUntil } from './flush';
import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils';
import { init } from './sdk';
import { classifyResponseStreaming } from './utils/streaming';

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

interface RequestHandlerWrapperOptions {
options: CloudflareOptions;
request: Request<unknown, IncomingRequestCfProperties<unknown> | CfProperties<unknown>>;
context: ExecutionContext | undefined;
context: ExecutionContextCompat | undefined;
/**
* If true, errors will be captured, rethrown and sent to Sentry.
* Otherwise, errors are rethrown but not captured.
Expand Down
46 changes: 35 additions & 11 deletions packages/cloudflare/src/workflows.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,12 +10,15 @@ import {
withScope,
} from '@sentry/core';
import type {
WorkflowDelayDuration,
WorkflowEntrypoint,
WorkflowEvent,
WorkflowSleepDuration,
WorkflowStep,
WorkflowStepConfig,
WorkflowStepContext,
WorkflowStepEvent,
WorkflowStepRollbackOptions,
WorkflowTimeoutDuration,
} from 'cloudflare:workers';
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
Expand DownExpand Up@@ -70,23 +73,34 @@ class WrappedWorkflowStep implements WorkflowStep {

public async do<T extends Rpc.Serializable<T>>(
name: string,
callback: (...args: unknown[]) => Promise<T>,
callback: (ctx: WorkflowStepContext) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
public async do<T extends Rpc.Serializable<T>, const C extends WorkflowStepConfig>(
name: string,
config: WorkflowStepConfig,
callback: (...args: unknown[]) => Promise<T>,
config: C,
callback: (
ctx: WorkflowStepContext<C['retries'] extends { delay: infer D } ? D : WorkflowDelayDuration | number>,
) => Promise<T>,
rollbackOptions?: WorkflowStepRollbackOptions<T>,
): Promise<T>;
public async do<T extends Rpc.Serializable<T>>(
name: string,
configOrCallback: WorkflowStepConfig | (() => Promise<T>),
maybeCallback?: (...args: unknown[]) => Promise<T>,
configOrCallback: WorkflowStepConfig | ((ctx: WorkflowStepContext) => Promise<T>),
callbackOrRollback?: ((ctx: WorkflowStepContext) => Promise<T>) | WorkflowStepRollbackOptions<T>,
maybeRollback?: WorkflowStepRollbackOptions<T>,
): Promise<T> {
// Capture the current scope, so parent span (e.g., a startSpan surrounding step.do) is preserved
const scopeForStep = getCurrentScope();

const userCallback = (maybeCallback || configOrCallback) as (...args: unknown[]) => Promise<T>;
const config = typeof configOrCallback === 'function' ? undefined : configOrCallback;
const hasConfig = typeof configOrCallback !== 'function';
const config = hasConfig ? configOrCallback : undefined;
const userCallback = (hasConfig ? callbackOrRollback : configOrCallback) as (
ctx: WorkflowStepContext,
) => Promise<T>;
const rollbackOptions = (hasConfig ? maybeRollback : callbackOrRollback) as
| WorkflowStepRollbackOptions<T>
| undefined;

const instrumentedCallback = async (...args: unknown[]): Promise<T> => {
// Feature detection: Cloudflare Workflows (April 2026+) pass a step context
Expand All@@ -109,7 +123,9 @@ class WrappedWorkflowStep implements WorkflowStep {
attributes: {
'cloudflare.workflow.timeout': config?.timeout,
'cloudflare.workflow.retries.backoff': config?.retries?.backoff,
'cloudflare.workflow.retries.delay': config?.retries?.delay,
// In workers-types v5, `delay` may be a `WorkflowDelayFunction`, which isn't a valid span attribute value.
'cloudflare.workflow.retries.delay':
typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay,
'cloudflare.workflow.retries.limit': config?.retries?.limit,
'cloudflare.workflow.attempt': attempt,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow',
Expand All@@ -118,7 +134,7 @@ class WrappedWorkflowStep implements WorkflowStep {
},
async span => {
try {
const result = await userCallback(...args);
const result = await (userCallback as (...args: unknown[]) => Promise<T>)(...args);
span.setStatus({ code: 1 });
return result;
} catch (error) {
Expand All@@ -133,7 +149,15 @@ class WrappedWorkflowStep implements WorkflowStep {
);
};

return config ? this._step.do(name, config, instrumentedCallback) : this._step.do(name, instrumentedCallback);
if (config) {
return rollbackOptions
? this._step.do(name, config, instrumentedCallback, rollbackOptions)
: this._step.do(name, config, instrumentedCallback);
}

return rollbackOptions
? this._step.do(name, instrumentedCallback, rollbackOptions)
: this._step.do(name, instrumentedCallback);
}

public async sleep(name: string, duration: WorkflowSleepDuration): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -3137,6 +3137,11 @@
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20250922.0.tgz#a159fbf3bb785fa85b473ecfaa8c501525827885"
integrity sha512-BaqlKnVc0Xzqm9xt3TC4v0yB9EHy5vVqpiWz+DAsbEmdcpUbqdBschvI9502p6FgFbZElD7XcxTEeViXLsoO0A==

"@cloudflare/workers-types@5.20260710.1":
version "5.20260710.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-5.20260710.1.tgz#215c0cf84c3917552b53a1f5129150abf0b6009f"
integrity sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==

"@cloudflare/workers-types@^4.20260426.0":
version "4.20260519.1"
resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20260519.1.tgz#061b4594e874a0e506ddc6599221939e6718d2a7"
Expand Down
Loading