diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts index 0c9e04919e42..8dfe934b04d5 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts @@ -2,5 +2,5 @@ // by running `wrangler types` interface Env { - E2E_TEST_DSN: ''; + E2E_TEST_DSN: string; } 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..fe12d39c7ac2 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 @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import * as Sentry from '@sentry/cloudflare'; -const app = new Hono(); +const app = new Hono<{ Bindings: Env }>(); app.get('/', ctx => { return ctx.json({ message: 'Welcome to Hono API' }); @@ -26,8 +26,8 @@ app.notFound(ctx => { }); export default Sentry.withSentry( - (env: Env) => ({ - dsn: env?.E2E_TEST_DSN, + env => ({ + dsn: env.E2E_TEST_DSN, tracesSampleRate: 1.0, }), app, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index 91a49e0788f4..5d7cfc35e469 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit", "cf-typegen": "wrangler types", "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test:dev && pnpm test:prod", + "test:assert": "pnpm typecheck && pnpm test:dev && pnpm test:prod", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test" }, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/env.d.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-workers/src/env.d.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts index ab438432a004..0c6e4464ae96 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts @@ -11,7 +11,7 @@ * Learn more at https://developers.cloudflare.com/workers/ */ import * as Sentry from '@sentry/cloudflare'; -import { DurableObject } from 'cloudflare:workers'; +import { DurableObject, WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from 'cloudflare:workers'; class MyDurableObjectBase extends DurableObject { private throwOnExit = new WeakMap(); @@ -53,7 +53,7 @@ class MyDurableObjectBase extends DurableObject { } export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( - (env: Env) => ({ + env => ({ dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server @@ -68,8 +68,27 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( MyDurableObjectBase, ); +class MyWorkflowBase extends WorkflowEntrypoint { + async run(_: WorkflowEvent, step: WorkflowStep) { + await step.do('send marketing follow up', async () => { + throw new Error('To be recorded in Sentry.'); + }); + } +} + +export const MyWorkflow = Sentry.instrumentWorkflowWithSentry( + env => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, + }), + MyWorkflowBase, +); + export default Sentry.withSentry( - (env: Env) => ({ + env => ({ dsn: env.E2E_TEST_DSN, environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json index f42019fb0915..4bb540caabac 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json @@ -36,7 +36,7 @@ /* Skip type checking all .d.ts files. */ "skipLibCheck": true, - "types": ["./worker-configuration.d.ts"] + "types": ["@cloudflare/workers-types/experimental"] }, "exclude": ["test"], "include": ["worker-configuration.d.ts", "src/**/*.ts"] diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 64467aad9d8f..f928a044ce15 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -154,6 +154,13 @@ function wrapMethodWithSentry( }); } +/** + * Helper type to extract the environment type from a DurableObject constructor. + * This extracts the second parameter type (env) from the constructor signature. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ExtractEnv = C extends new (state: any, env: infer E) => any ? E : never; + /** * Instruments a Durable Object class to capture errors and performance data. * @@ -188,10 +195,9 @@ function wrapMethodWithSentry( * ``` */ export function instrumentDurableObjectWithSentry< - E, - T extends DurableObject, - C extends new (state: DurableObjectState, env: E) => T, ->(optionsCallback: (env: E) => CloudflareOptions, DurableObjectClass: C): C { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + C extends new (state: DurableObjectState, env: any) => DurableObject, +>(optionsCallback: (env: ExtractEnv) => CloudflareOptions, DurableObjectClass: C): C { return new Proxy(DurableObjectClass, { construct(target, [ctx, env]) { setAsyncLocalStorageAsyncContextStrategy(); @@ -332,6 +338,7 @@ function instrumentPrototype(target: T, methodsToInst } // Create a wrapper that gets context/options from the instance at runtime + // eslint-disable-next-line @typescript-eslint/no-explicit-any const wrappedMethod = function (this: any, ...args: any[]): unknown { const thisWithSentry = this as { __SENTRY_CONTEXT__: DurableObjectState; @@ -342,6 +349,7 @@ function instrumentPrototype(target: T, methodsToInst if (!instanceOptions) { // Fallback to original method if no Sentry data found + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (originalMethod as (...args: any[]) => any).apply(this, args); } @@ -353,11 +361,13 @@ function instrumentPrototype(target: T, methodsToInst spanName: methodName, spanOp: 'rpc', }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any originalMethod as (...args: any[]) => any, undefined, true, // noMark = true since we'll mark the prototype method ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (wrapper as (...args: any[]) => any).apply(this, args); }; diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 8c6d02791d0f..289a97a684d9 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -17,6 +17,39 @@ import { addCloudResourceContext } from './scope-utils'; import { init } from './sdk'; import { copyExecutionContext } from './utils/copyExecutionContext'; +/** + * Helper type to filter out empty object types from a union + * This is a distributive conditional type that operates on each member of a union + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FilterEmptyObjects = T extends any ? ([keyof T] extends [never] ? never : T) : never; + +/** + * Helper type to extract Env from ExportedHandler + * Filters out empty object types from unions and returns unknown instead + */ +type InferEnv = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends ExportedHandler + ? unknown extends E + ? unknown + : FilterEmptyObjects extends never + ? unknown + : FilterEmptyObjects + : unknown; + +/** + * Helper type to extract QueueHandlerMessage from ExportedHandler + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type InferQueueMessage = T extends ExportedHandler ? Q : unknown; + +/** + * Helper type to extract CfHostMetadata from ExportedHandler + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type InferCfHostMetadata = T extends ExportedHandler ? C : unknown; + /** * Wrapper for Cloudflare handlers. * @@ -28,17 +61,14 @@ import { copyExecutionContext } from './utils/copyExecutionContext'; * @param handler {ExportedHandler} The handler to wrap. * @returns The wrapped handler. */ -// eslint-disable-next-line complexity -export function withSentry< - Env = unknown, - QueueHandlerMessage = unknown, - CfHostMetadata = unknown, - T extends ExportedHandler = ExportedHandler< - Env, - QueueHandlerMessage, - CfHostMetadata - >, ->(optionsCallback: (env: Env) => CloudflareOptions, handler: T): T { +// eslint-disable-next-line complexity, @typescript-eslint/no-explicit-any +export function withSentry>( + optionsCallback: (env: InferEnv) => CloudflareOptions, + handler: T, +): T { + type Env = InferEnv; + type QueueHandlerMessage = InferQueueMessage; + type CfHostMetadata = InferCfHostMetadata; setAsyncLocalStorageAsyncContextStrategy(); try { diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 17ec17e9cd85..497ebc946ee6 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -132,6 +132,20 @@ class WrappedWorkflowStep implements WorkflowStep { } } +/** + * Helper type to extract the environment type from a WorkflowEntrypoint constructor. + * This extracts the second parameter type (env) from the constructor signature. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ExtractEnv = C extends new (ctx: any, env: infer E) => any ? E : never; + +/** + * Helper type to extract the payload type from a WorkflowEntrypoint constructor. + * This extracts the second parameter type (P) from the constructor signature. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ExtractPayload = C extends new (ctx: any, env: any, payload: infer P) => any ? P : never; + /** * Instruments a Cloudflare Workflow class with Sentry. * @@ -150,13 +164,15 @@ class WrappedWorkflowStep implements WorkflowStep { * @returns Instrumented workflow class with the same interface */ export function instrumentWorkflowWithSentry< - E, // Environment type - P, // Payload type - T extends WorkflowEntrypoint, // WorkflowEntrypoint type - C extends new (ctx: ExecutionContext, env: E) => T, // Constructor type of the WorkflowEntrypoint class ->(optionsCallback: (env: E) => CloudflareOptions, WorkFlowClass: C): C { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + C extends new (ctx: ExecutionContext, env: any) => WorkflowEntrypoint, +>(optionsCallback: (env: ExtractEnv) => CloudflareOptions, WorkFlowClass: C): C { + type Env = ExtractEnv; + type Payload = ExtractPayload; + type T = WorkflowEntrypoint; + return new Proxy(WorkFlowClass, { - construct(target: C, args: [ctx: ExecutionContext, env: E], newTarget) { + construct(target: C, args: [ctx: ExecutionContext, env: Env], newTarget) { const [ctx, env] = args; const context = copyExecutionContext(ctx); args[0] = context; @@ -166,7 +182,7 @@ export function instrumentWorkflowWithSentry< return new Proxy(instance, { get(obj, prop, receiver) { if (prop === 'run') { - return async function (event: WorkflowEvent

, step: WorkflowStep): Promise { + return async function (event: WorkflowEvent, step: WorkflowStep): Promise { setAsyncLocalStorageAsyncContextStrategy(); return withIsolationScope(async isolationScope => {