From dfbd1caf5f92c0f8f1938e8c30cf115d189dd035 Mon Sep 17 00:00:00 2001 From: cod1k Date: Thu, 25 Sep 2025 09:45:29 +0300 Subject: [PATCH 1/7] Add abort signals to test runners Updated test definitions across multiple suites to accept and pass abort signals (`signal`) for enhanced request handling. Adjusted the `start` method in the test runner to support optional abort signals. --- dev-packages/cloudflare-integration-tests/runner.ts | 4 ++-- .../cloudflare-integration-tests/suites/basic/test.ts | 4 ++-- .../suites/tracing/anthropic-ai/test.ts | 4 ++-- .../suites/tracing/durableobject/test.ts | 4 ++-- .../suites/tracing/openai/test.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts index 849b011250f9..b945bee2eeea 100644 --- a/dev-packages/cloudflare-integration-tests/runner.ts +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -86,7 +86,7 @@ export function createRunner(...paths: string[]) { } return this; }, - start: function (): StartResult { + start: function (signal?: AbortSignal): StartResult { const { resolve, reject, promise: isComplete } = deferredPromise(cleanupChildProcesses); const expectedEnvelopeCount = expectedEnvelopes.length; @@ -155,7 +155,7 @@ export function createRunner(...paths: string[]) { '--var', `SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`, ], - { stdio }, + { stdio, signal }, ); CLEANUP_STEPS.add(() => { diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts index b785e6e37fd1..347c0d3530d8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import { eventEnvelope } from '../../expect'; import { createRunner } from '../../runner'; -it('Basic error in fetch handler', async () => { +it('Basic error in fetch handler', async ({ signal }) => { const runner = createRunner(__dirname) .expect( eventEnvelope({ @@ -26,7 +26,7 @@ it('Basic error in fetch handler', async () => { }, }), ) - .start(); + .start(signal); await runner.makeRequest('get', '/', { expectError: true }); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts index 13966caaf460..c9e112b32241 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts @@ -6,7 +6,7 @@ import { createRunner } from '../../../runner'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces a basic message creation request', async () => { +it('traces a basic message creation request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { @@ -35,7 +35,7 @@ it('traces a basic message creation request', async () => { ]), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/'); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts index a9daae21480f..e86508c0f101 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; -it('traces a durable object method', async () => { +it('traces a durable object method', async ({ signal }) => { const runner = createRunner(__dirname) .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1]; @@ -21,7 +21,7 @@ it('traces a durable object method', async () => { }), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/hello'); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts index c1aee24136a4..eb15fd80fc97 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts @@ -6,7 +6,7 @@ import { createRunner } from '../../../runner'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces a basic chat completion request', async () => { +it('traces a basic chat completion request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { @@ -37,7 +37,7 @@ it('traces a basic chat completion request', async () => { ]), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/'); await runner.completed(); }); From aae2dd3f6d42b200fba12ba0d804d51db6ecad5b Mon Sep 17 00:00:00 2001 From: cod1k Date: Thu, 25 Sep 2025 17:09:06 +0300 Subject: [PATCH 2/7] Refactor `copyExecutionContext` for improved flexibility Reworked `copyExecutionContext` to use a dynamic property descriptor approach, enabling method overrides without altering the original object. Expanded test cases to cover additional methods and verify override behavior. --- .../src/utils/copyExecutionContext.ts | 51 +++++++++++++++++ .../test/copy-execution-context.test.ts | 56 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/cloudflare/src/utils/copyExecutionContext.ts create mode 100644 packages/cloudflare/test/copy-execution-context.test.ts diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts new file mode 100644 index 000000000000..2ba9d2e36a2d --- /dev/null +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -0,0 +1,51 @@ +import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types'; + +type ContextType = ExecutionContext | DurableObjectState; +type OverridesStore = Map unknown>; + +/** + * Creates a new copy of the given execution context, optionally overriding methods. + * + * @param {ContextType|void} ctx - The execution context to be copied. Can be of type `ContextType` or `void`. + * @return {ContextType|void} A new execution context with the same properties and overridden methods if applicable. + */ +export function copyExecutionContext(ctx: T): T { + if (!ctx) return ctx; + + const overrides: OverridesStore = new Map(); + const contextPrototype = Object.getPrototypeOf(ctx); + const descriptors = Object.getOwnPropertyNames(contextPrototype).reduce((prevDescriptors, methodName) => { + if (methodName === 'constructor') return prevDescriptors; + const pd = makeMethodDescriptor(overrides, ctx, methodName as keyof ContextType); + return { + ...prevDescriptors, + [methodName]: pd, + }; + }, {}); + + return Object.create(ctx, descriptors); +} + +/** + * Creates a property descriptor for a given method on a context object, enabling custom getter and setter behavior. + * + * @param store - The OverridesStore instance used to manage method overrides. + * @param ctx - The context object from which the method originates. + * @param method - The key of the method on the context object to create a descriptor for. + * @return A property descriptor with custom getter and setter functionalities for the specified method. + */ +function makeMethodDescriptor(store: OverridesStore, ctx: ContextType, method: keyof ContextType): PropertyDescriptor { + return { + configurable: true, + enumerable: true, + set: newValue => { + store.set(method, newValue); + return true; + }, + + get: () => { + if (store.has(method)) return store.get(method); + return Reflect.get(ctx, method).bind(ctx); + }, + }; +} diff --git a/packages/cloudflare/test/copy-execution-context.test.ts b/packages/cloudflare/test/copy-execution-context.test.ts new file mode 100644 index 000000000000..3ee71a10b695 --- /dev/null +++ b/packages/cloudflare/test/copy-execution-context.test.ts @@ -0,0 +1,56 @@ +import { type Mocked, describe, expect, it, vi } from 'vitest'; +import { copyExecutionContext } from '../src/utils/copyExecutionContext'; + +describe('Copy of the execution context', () => { + describe.for([ + 'waitUntil', + 'passThroughOnException', + 'acceptWebSocket', + 'blockConcurrencyWhile', + 'getWebSockets', + 'arbitraryMethod', + 'anythingElse', + ])('%s', method => { + it('Override without changing original', async () => { + const context = { + [method]: vi.fn(), + } as any; + const copy = copyExecutionContext(context); + copy[method] = vi.fn(); + expect(context[method]).not.toBe(copy[method]); + }); + + it('Overridden method was called', async () => { + const context = { + [method]: vi.fn(), + } as any; + const copy = copyExecutionContext(context); + const overridden = vi.fn(); + copy[method] = overridden; + copy[method](); + expect(overridden).toBeCalled(); + expect(context[method]).not.toBeCalled(); + }); + }); + + it('No side effects', async () => { + const context = makeExecutionContextMock(); + expect(() => copyExecutionContext(Object.freeze(context))).not.toThrow( + /Cannot define property \w+, object is not extensible/, + ); + }); + it('Respects symbols', async () => { + const s = Symbol('test'); + const context = makeExecutionContextMock(); + context[s] = {}; + const copy = copyExecutionContext(context); + expect(copy[s]).toBe(context[s]); + }); +}); + +function makeExecutionContextMock() { + return { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + } as unknown as Mocked; +} From 5854923b412343d630322cf6197d078be732a6b7 Mon Sep 17 00:00:00 2001 From: cod1k Date: Thu, 25 Sep 2025 18:02:34 +0300 Subject: [PATCH 3/7] Improve `copyExecutionContext` method handling Enhanced the descriptor logic to better handle non-function properties and prevent invalid method overrides. Fixed potential issues with binding non-function properties to the context. --- packages/cloudflare/src/utils/copyExecutionContext.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts index 2ba9d2e36a2d..bb49ce686a1e 100644 --- a/packages/cloudflare/src/utils/copyExecutionContext.ts +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -39,13 +39,16 @@ function makeMethodDescriptor(store: OverridesStore, ctx: ContextType, method: k configurable: true, enumerable: true, set: newValue => { + if(typeof newValue !== 'function') throw new Error('Cannot override non-function') store.set(method, newValue); return true; }, get: () => { if (store.has(method)) return store.get(method); - return Reflect.get(ctx, method).bind(ctx); + const methodFunction = Reflect.get(ctx, method); + if (typeof methodFunction !== 'function') return methodFunction; + return methodFunction.bind(ctx); }, }; } From c0230eabf9e6f118e07e76d67c6486ec342b015a Mon Sep 17 00:00:00 2001 From: cod1k Date: Fri, 26 Sep 2025 09:17:09 +0300 Subject: [PATCH 4/7] Refactor `copyExecutionContext` for type safety and clarity Updated the `OverridesStore` type to strictly associate keys with context-specific methods, improving type safety. Renamed and enhanced the descriptor creation function to better handle method overriding and added checks for function-only properties. --- .../src/utils/copyExecutionContext.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts index bb49ce686a1e..611841127251 100644 --- a/packages/cloudflare/src/utils/copyExecutionContext.ts +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -1,7 +1,7 @@ import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types'; type ContextType = ExecutionContext | DurableObjectState; -type OverridesStore = Map unknown>; +type OverridesStore = Map unknown>; /** * Creates a new copy of the given execution context, optionally overriding methods. @@ -12,14 +12,16 @@ type OverridesStore = Map unknown>; export function copyExecutionContext(ctx: T): T { if (!ctx) return ctx; - const overrides: OverridesStore = new Map(); + const overrides: OverridesStore = new Map(); const contextPrototype = Object.getPrototypeOf(ctx); - const descriptors = Object.getOwnPropertyNames(contextPrototype).reduce((prevDescriptors, methodName) => { + const methodNames = Object.getOwnPropertyNames(contextPrototype) as unknown as (keyof T)[]; + const descriptors = methodNames.reduce((prevDescriptors, methodName) => { if (methodName === 'constructor') return prevDescriptors; - const pd = makeMethodDescriptor(overrides, ctx, methodName as keyof ContextType); + if (typeof ctx[methodName] !== 'function') return prevDescriptors; + const overridableDescriptor = makeOverridableDescriptor(overrides, ctx, methodName); return { ...prevDescriptors, - [methodName]: pd, + [methodName]: overridableDescriptor, }; }, {}); @@ -27,19 +29,26 @@ export function copyExecutionContext(ctx: T): T { } /** - * Creates a property descriptor for a given method on a context object, enabling custom getter and setter behavior. + * Creates a property descriptor that allows overriding of a method on the given context object. * - * @param store - The OverridesStore instance used to manage method overrides. - * @param ctx - The context object from which the method originates. - * @param method - The key of the method on the context object to create a descriptor for. - * @return A property descriptor with custom getter and setter functionalities for the specified method. + * This descriptor supports property overriding with functions only. It delegates method calls to + * the provided store if an override exists or to the original method on the context otherwise. + * + * @param {OverridesStore} store - The storage for overridden methods specific to the context type. + * @param {ContextType} ctx - The context object that contains the method to be overridden. + * @param {keyof ContextType} method - The method on the context object to create the overridable descriptor for. + * @return {PropertyDescriptor} A property descriptor enabling the overriding of the specified method. */ -function makeMethodDescriptor(store: OverridesStore, ctx: ContextType, method: keyof ContextType): PropertyDescriptor { +function makeOverridableDescriptor( + store: OverridesStore, + ctx: T, + method: keyof T, +): PropertyDescriptor { return { configurable: true, enumerable: true, set: newValue => { - if(typeof newValue !== 'function') throw new Error('Cannot override non-function') + if (typeof newValue !== 'function') throw new Error('Cannot override non-function'); store.set(method, newValue); return true; }, From 6e626745aca59b958428c7329b106c0e648d8198 Mon Sep 17 00:00:00 2001 From: cod1k Date: Tue, 30 Sep 2025 15:53:08 +0300 Subject: [PATCH 5/7] Added a notation why do we need to do "bind()" here --- packages/cloudflare/src/utils/copyExecutionContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts index 611841127251..1f1c09aa656d 100644 --- a/packages/cloudflare/src/utils/copyExecutionContext.ts +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -57,6 +57,7 @@ function makeOverridableDescriptor( if (store.has(method)) return store.get(method); const methodFunction = Reflect.get(ctx, method); if (typeof methodFunction !== 'function') return methodFunction; + // We should do bind() to make sure that the method is bound to the context object - otherwise it will not work return methodFunction.bind(ctx); }, }; From 7121b6566d434dbe9dc25928dc5c6cb27eb055fb Mon Sep 17 00:00:00 2001 From: cod1k Date: Tue, 30 Sep 2025 16:00:08 +0300 Subject: [PATCH 6/7] Ignore JUnit report files in version control Added `packages/**/*.junit.xml` to `.gitignore` to prevent JUnit report files from being tracked in the repository, keeping the working directory clean and avoiding unnecessary versioning of test artifacts. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f381e7e6e24d..36f8a3f6b9fe 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ packages/gatsby/gatsby-node.d.ts # intellij *.iml /**/.wrangler/* + +#junit reports +packages/**/*.junit.xml From 224a1d6429964909c303db85d80e1710144081b3 Mon Sep 17 00:00:00 2001 From: cod1k Date: Mon, 13 Oct 2025 09:38:19 +0300 Subject: [PATCH 7/7] Integrate `copyExecutionContext` across handlers and workflows Replaced direct usage of `ExecutionContext` with `copyExecutionContext` for consistency and improved flexibility. Applied changes across handlers, durable objects, and workflows to enhance context management and method overriding. --- packages/cloudflare/src/durableobject.ts | 4 +++- packages/cloudflare/src/handler.ts | 23 +++++++++++++++---- .../src/utils/copyExecutionContext.ts | 17 +++++++++----- packages/cloudflare/src/workflows.ts | 8 +++++-- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 0f139a80ccd0..64467aad9d8f 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -18,6 +18,7 @@ import { isInstrumented, markAsInstrumented } from './instrument'; import { getFinalOptions } from './options'; import { wrapRequestHandler } from './request'; import { init } from './sdk'; +import { copyExecutionContext } from './utils/copyExecutionContext'; type MethodWrapperOptions = { spanName?: string; @@ -192,8 +193,9 @@ export function instrumentDurableObjectWithSentry< C extends new (state: DurableObjectState, env: E) => T, >(optionsCallback: (env: E) => CloudflareOptions, DurableObjectClass: C): C { return new Proxy(DurableObjectClass, { - construct(target, [context, env]) { + construct(target, [ctx, env]) { setAsyncLocalStorageAsyncContextStrategy(); + const context = copyExecutionContext(ctx); const options = getFinalOptions(optionsCallback(env), env); diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 969cb6be72ee..e3e108b913d7 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -15,6 +15,7 @@ import { getFinalOptions } from './options'; import { wrapRequestHandler } from './request'; import { addCloudResourceContext } from './scope-utils'; import { init } from './sdk'; +import { copyExecutionContext } from './utils/copyExecutionContext'; /** * Wrapper for Cloudflare handlers. @@ -38,7 +39,9 @@ export function withSentry>) { - const [request, env, context] = args; + const [request, env, ctx] = args; + const context = copyExecutionContext(ctx); + args[2] = context; const options = getFinalOptions(optionsCallback(env), env); @@ -72,7 +75,10 @@ export function withSentry>) { - const [event, env, context] = args; + const [event, env, ctx] = args; + const context = copyExecutionContext(ctx); + args[2] = context; + return withIsolationScope(isolationScope => { const options = getFinalOptions(optionsCallback(env), env); const waitUntil = context.waitUntil.bind(context); @@ -115,7 +121,10 @@ export function withSentry>) { - const [emailMessage, env, context] = args; + const [emailMessage, env, ctx] = args; + const context = copyExecutionContext(ctx); + args[2] = context; + return withIsolationScope(isolationScope => { const options = getFinalOptions(optionsCallback(env), env); const waitUntil = context.waitUntil.bind(context); @@ -156,7 +165,9 @@ export function withSentry>) { - const [batch, env, context] = args; + const [batch, env, ctx] = args; + const context = copyExecutionContext(ctx); + args[2] = context; return withIsolationScope(isolationScope => { const options = getFinalOptions(optionsCallback(env), env); @@ -206,7 +217,9 @@ export function withSentry>) { - const [, env, context] = args; + const [, env, ctx] = args; + const context = copyExecutionContext(ctx); + args[2] = context; return withIsolationScope(async isolationScope => { const options = getFinalOptions(optionsCallback(env), env); diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts index 1f1c09aa656d..85a007f16e18 100644 --- a/packages/cloudflare/src/utils/copyExecutionContext.ts +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -14,10 +14,13 @@ export function copyExecutionContext(ctx: T): T { const overrides: OverridesStore = new Map(); const contextPrototype = Object.getPrototypeOf(ctx); - const methodNames = Object.getOwnPropertyNames(contextPrototype) as unknown as (keyof T)[]; - const descriptors = methodNames.reduce((prevDescriptors, methodName) => { - if (methodName === 'constructor') return prevDescriptors; + const prototypeMethodNames = Object.getOwnPropertyNames(contextPrototype) as unknown as (keyof T)[]; + const ownPropertyNames = Object.getOwnPropertyNames(ctx) as unknown as (keyof T)[]; + const instrumented = new Set(['constructor']); + const descriptors = [...ownPropertyNames, ...prototypeMethodNames].reduce((prevDescriptors, methodName) => { + if (instrumented.has(methodName)) return prevDescriptors; if (typeof ctx[methodName] !== 'function') return prevDescriptors; + instrumented.add(methodName); const overridableDescriptor = makeOverridableDescriptor(overrides, ctx, methodName); return { ...prevDescriptors, @@ -48,9 +51,11 @@ function makeOverridableDescriptor( configurable: true, enumerable: true, set: newValue => { - if (typeof newValue !== 'function') throw new Error('Cannot override non-function'); - store.set(method, newValue); - return true; + if (typeof newValue == 'function') { + store.set(method, newValue); + return; + } + Reflect.set(ctx, method, newValue); }, get: () => { diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 16327ea71ccf..17ec17e9cd85 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -22,6 +22,7 @@ import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import { addCloudResourceContext } from './scope-utils'; import { init } from './sdk'; +import { copyExecutionContext } from './utils/copyExecutionContext'; const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i; @@ -157,6 +158,9 @@ export function instrumentWorkflowWithSentry< return new Proxy(WorkFlowClass, { construct(target: C, args: [ctx: ExecutionContext, env: E], newTarget) { const [ctx, env] = args; + const context = copyExecutionContext(ctx); + args[0] = context; + const options = optionsCallback(env); const instance = Reflect.construct(target, args, newTarget) as T; return new Proxy(instance, { @@ -179,10 +183,10 @@ export function instrumentWorkflowWithSentry< return await obj.run.call( obj, event, - new WrappedWorkflowStep(event.instanceId, ctx, options, step), + new WrappedWorkflowStep(event.instanceId, context, options, step), ); } finally { - ctx.waitUntil(flush(2000)); + context.waitUntil(flush(2000)); } }); });