From d51db5c0d68717592ef7ed8d4ddebe07d73cdb49 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 23 Jun 2025 16:13:18 -0400 Subject: [PATCH] fix(cloudflare): Import types explicitly from worker-types --- .../cloudflare-hono/package.json | 2 +- .../cloudflare-hono/src/index.ts | 13 +++ packages/cloudflare/src/durableobject.ts | 5 +- packages/cloudflare/src/handler.ts | 8 ++ packages/cloudflare/src/pages-plugin.ts | 11 ++- packages/cloudflare/src/request.ts | 8 +- packages/cloudflare/src/scope-utils.ts | 4 +- packages/cloudflare/src/vendor/workflow.ts | 87 +++++++++++++++++++ packages/cloudflare/src/workflows.ts | 11 +-- packages/cloudflare/tsconfig.json | 2 +- packages/sveltekit/src/worker/cloudflare.ts | 3 +- 11 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 packages/cloudflare/src/vendor/workflow.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json index b9667aeef85f..35289ae976cd 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.31", - "@cloudflare/workers-types": "^4.20250521.0", + "@cloudflare/workers-types": "^4.20250620.0", "vitest": "3.1.0", "wrangler": "^4.16.0" }, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts index 7cd667c72408..c6570b26d60d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts @@ -25,6 +25,19 @@ app.notFound(ctx => { return ctx.json({ message: 'Not Found' }, 404); }); +class MyDurableObjectBase extends DurableObject { + // impl +} + +// Typecheck that the instrumented durable object is valid +export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env?.E2E_TEST_DSN, + tracesSampleRate: 1.0, + }), + MyDurableObjectBase, +); + export default Sentry.withSentry( (env: Env) => ({ dsn: env?.E2E_TEST_DSN, diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 35fbb5096a41..1cdbb103cd68 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/unbound-method */ +import type { DurableObject, DurableObjectState, ExecutionContext } from '@cloudflare/workers-types'; import { captureException, flush, @@ -9,7 +9,6 @@ import { withIsolationScope, withScope, } from '@sentry/core'; -import type { DurableObject } from 'cloudflare:workers'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import { isInstrumented, markAsInstrumented } from './instrument'; @@ -135,7 +134,7 @@ function wrapMethodWithSentry any>( */ export function instrumentDurableObjectWithSentry< E, - T extends DurableObject, + T extends Partial, C extends new (state: DurableObjectState, env: E) => T, >(optionsCallback: (env: E) => CloudflareOptions, DurableObjectClass: C): C { return new Proxy(DurableObjectClass, { diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index d3d1f80dbbd5..b0919437ae67 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -1,3 +1,11 @@ +import type { + EmailExportedHandler, + ExportedHandler, + ExportedHandlerFetchHandler, + ExportedHandlerQueueHandler, + ExportedHandlerScheduledHandler, + ExportedHandlerTailHandler, +} from '@cloudflare/workers-types'; import { captureException, flush, diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index fe784194d411..90660cde6cf9 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -1,3 +1,4 @@ +import type { EventPluginContext, PagesPluginFunction } from '@cloudflare/workers-types'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; import type { CloudflareOptions } from './client'; import { wrapRequestHandler } from './request'; @@ -49,8 +50,12 @@ export function sentryPagesPlugin< setAsyncLocalStorageAsyncContextStrategy(); return context => { const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions; - return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () => - context.next(), - ); + return wrapRequestHandler( + { options, request: context.request, context: { ...context, props: {} } }, + () => context.next() as unknown as Promise, + // Need to use `any` here because the return type does not work with Cloudflare's Response type. + // Returning `PagesPluginFunction` in the wrapper function should take care of type safety. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; }; } diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index f1905609fb94..f36b2a8e294a 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -1,4 +1,8 @@ -import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; +import type { + ExecutionContext, + IncomingRequestCfProperties, + Request as CloudflareRequest, +} from '@cloudflare/workers-types'; import { captureException, continueTrace, @@ -16,7 +20,7 @@ import { init } from './sdk'; interface RequestHandlerWrapperOptions { options: CloudflareOptions; - request: Request>; + request: CloudflareRequest>; context: ExecutionContext; } diff --git a/packages/cloudflare/src/scope-utils.ts b/packages/cloudflare/src/scope-utils.ts index 9a3f7d286dfc..f014cfaf9052 100644 --- a/packages/cloudflare/src/scope-utils.ts +++ b/packages/cloudflare/src/scope-utils.ts @@ -1,4 +1,4 @@ -import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; +import type { IncomingRequestCfProperties, Request as CloudflareRequest } from '@cloudflare/workers-types'; import type { Scope } from '@sentry/core'; import { winterCGRequestToRequestData } from '@sentry/core'; @@ -23,6 +23,6 @@ export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties) /** * Set request data on scope */ -export function addRequest(scope: Scope, request: Request): void { +export function addRequest(scope: Scope, request: CloudflareRequest): void { scope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(request) }); } diff --git a/packages/cloudflare/src/vendor/workflow.ts b/packages/cloudflare/src/vendor/workflow.ts new file mode 100644 index 000000000000..695fdaa68532 --- /dev/null +++ b/packages/cloudflare/src/vendor/workflow.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +// This file vendors in worker types in `cloudflare:workers` because they are not +// exported from `@cloudflare/workers-types` yet. + +/* ! ***************************************************************************** +Copyright (c) Cloudflare. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + +import type { ExecutionContext, Rpc } from '@cloudflare/workers-types'; + +const __WORKFLOW_ENTRYPOINT_BRAND = '__WORKFLOW_ENTRYPOINT_BRAND' as const; + +export type WorkflowEvent = { + payload: Readonly; + timestamp: Date; + instanceId: string; +}; + +export type WorkflowStepEvent = { + payload: Readonly; + timestamp: Date; + type: string; +}; + +export type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; +export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number; +export type WorkflowDelayDuration = WorkflowSleepDuration; +export type WorkflowTimeoutDuration = WorkflowSleepDuration; +export type WorkflowRetentionDuration = WorkflowSleepDuration; + +export type WorkflowBackoff = 'constant' | 'linear' | 'exponential'; + +export type WorkflowStepConfig = { + retries?: { + limit: number; + delay: WorkflowDelayDuration | number; + backoff?: WorkflowBackoff; + }; + timeout?: WorkflowTimeoutDuration | number; +}; + +export abstract class WorkflowStep { + do>(name: string, callback: () => Promise): Promise; + // @ts-expect-error - This is a vendor file, so we don't need to implement the method. + do>(name: string, config: WorkflowStepConfig, callback: () => Promise): Promise; + // @ts-expect-error - This is a vendor file, so we don't need to implement the method. + sleep: (name: string, duration: WorkflowSleepDuration) => Promise; + // @ts-expect-error - This is a vendor file, so we don't need to implement the method. + sleepUntil: (name: string, timestamp: Date | number) => Promise; + // @ts-expect-error - This is a vendor file, so we don't need to implement the method. + waitForEvent>( + name: string, + options: { + type: string; + timeout?: WorkflowTimeoutDuration | number; + }, + ): Promise>; +} + +export abstract class WorkflowEntrypoint | unknown = unknown> + implements Rpc.WorkflowEntrypointBranded +{ + // @ts-expect-error - This is a vendor file, so we don't need to implement the property. + [__WORKFLOW_ENTRYPOINT_BRAND]: never; + // @ts-expect-error - This is a vendor file, so we don't need to implement the property. + protected ctx: ExecutionContext; + // @ts-expect-error - This is a vendor file, so we don't need to implement the property. + protected env: Env; + // @ts-expect-error - This is a vendor file, so we don't need to implement the constructor. + constructor(ctx: ExecutionContext, env: Env); + // @ts-expect-error - This is a vendor file, so we don't need to implement the method. + run(event: Readonly>, step: WorkflowStep): Promise; +} diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 022f0040893a..7d67c4f6e2d9 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -1,3 +1,4 @@ +import type { ExecutionContext, Rpc } from '@cloudflare/workers-types'; import type { PropagationContext } from '@sentry/core'; import { captureException, @@ -8,6 +9,10 @@ import { withIsolationScope, withScope, } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import type { CloudflareOptions } from './client'; +import { addCloudResourceContext } from './scope-utils'; +import { init } from './sdk'; import type { WorkflowEntrypoint, WorkflowEvent, @@ -16,11 +21,7 @@ import type { WorkflowStepConfig, WorkflowStepEvent, WorkflowTimeoutDuration, -} from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from './async'; -import type { CloudflareOptions } from './client'; -import { addCloudResourceContext } from './scope-utils'; -import { init } from './sdk'; +} from './vendor/workflow'; const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i; diff --git a/packages/cloudflare/tsconfig.json b/packages/cloudflare/tsconfig.json index ff89f0feaa23..3df16a27d6de 100644 --- a/packages/cloudflare/tsconfig.json +++ b/packages/cloudflare/tsconfig.json @@ -5,6 +5,6 @@ "compilerOptions": { "module": "esnext", - "types": ["node", "@cloudflare/workers-types"] + "types": ["node"] } } diff --git a/packages/sveltekit/src/worker/cloudflare.ts b/packages/sveltekit/src/worker/cloudflare.ts index e3d93e8b8922..17580eb9c83f 100644 --- a/packages/sveltekit/src/worker/cloudflare.ts +++ b/packages/sveltekit/src/worker/cloudflare.ts @@ -34,7 +34,8 @@ export function initCloudflareSentryHandle(options: CloudflareOptions): Handle { return wrapRequestHandler( { options: opts, - request: event.request as Request>, + // @ts-expect-error Cloudflare's `request` type is not compatible with `Request` type. + request: event.request, // @ts-expect-error This will exist in Cloudflare context: event.platform.context, },